﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;

namespace SynoManager.DownloadStation.Ftp
{
    public sealed class FtpClient
    {
        private readonly string host;
        private readonly string username;
        private readonly string password;
        private readonly bool usePassive;
        private ICredentials credentials;

        public FtpClient(string host, string username, string password, bool usePassive)
        {
            this.host = host;
            this.username = username;
            this.password = password;
            this.usePassive = usePassive;

            credentials = new NetworkCredential(username, password);
        }

        public string Host
        {
            get { return host; }
        }

        public string Username
        {
            get { return username; }
        }

        public string Password
        {
            get { return password; }
        }

        public FtpDirectoryInfo GetRoot()
        {
            return GetDirectory("/");
        }

        public FtpDirectoryInfo GetDirectory(string relativePath)
        {
            return new FtpDirectoryInfo(this, relativePath);
        }

        internal string ExecuteFtpCommand(string relativePath, string method)
        {
            Debug.WriteLine(method + ": " + relativePath, "FtpClient");
            string requestUri = String.Format("ftp://{0}{1}", Host, relativePath);
            FtpWebRequest request = (FtpWebRequest)WebRequest.Create(requestUri);
            request.Credentials = credentials;
            request.Method = method;
            request.UsePassive = usePassive;

            OnCommandSent(new FtpCommandEventArgs(method, ""));

            FtpWebResponse response = (FtpWebResponse)request.GetResponse();
            StreamReader reader = new StreamReader(response.GetResponseStream());
            string result = reader.ReadToEnd().Replace("\r\n", "\n").TrimEnd('\n');

            OnResultReceived(new FtpCommandEventArgs(method, result));

            return result;
        }

        public event EventHandler<FtpCommandEventArgs> CommandSent;
        public event EventHandler<FtpCommandEventArgs> ResultReceived;

        private void OnCommandSent(FtpCommandEventArgs e)
        {
            if (CommandSent!=null)
                CommandSent(this, e);
        }

        private void OnResultReceived(FtpCommandEventArgs e)
        {
            if (ResultReceived != null)
                ResultReceived(this, e);
        }
    }

    public class FtpCommandEventArgs : EventArgs
    {
        private readonly string command;
        private readonly string text;

        public FtpCommandEventArgs(string command, string text)
        {
            this.command = command;
            this.text = text;
        }

        public string Command
        {
            get { return command; }
        }

        public string Text
        {
            get { return text; }
        }
    }

    public class FtpDirectoryInfo : IEnumerable<FtpDirectoryEntry>
    {
        private readonly FtpClient client;
        private readonly string relativePath;

        internal FtpDirectoryInfo(FtpClient client, string relativePath)
        {
            this.client = client;
            this.relativePath = relativePath;

            string result = client.ExecuteFtpCommand(relativePath, "LIST");

            entries = new List<FtpDirectoryEntry>();

            foreach (string line in result.Split('\n'))
            {
                entries.Add(new FtpDirectoryEntry(relativePath, line));
            }
        }

        private List<FtpDirectoryEntry> entries;
        public FtpDirectoryEntry[] GetDirectories()
        {
            return entries.Where(e=>e.IsDirectory).ToArray();
        }

        public FtpDirectoryEntry[] GetFiles()
        {
            return entries.Where(e => !e.IsDirectory).ToArray();
        }

        #region IEnumerable<FtpDirectoryEntry> Members

        public IEnumerator<FtpDirectoryEntry> GetEnumerator()
        {
            return entries.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return entries.GetEnumerator();
        }

        #endregion
    }

    public class FtpDirectoryEntry
    {
        private readonly string directory;
        private readonly bool isDir;
        private readonly string name;
        private readonly long size;
        private readonly string permissions;

        internal FtpDirectoryEntry(string directory, string line)
        {
            this.directory = directory;

            Match m = GetMatchingRegex(line);
            string dir = m.Groups["dir"].Value;

            isDir = (dir != "") && (dir != "-");
            name = m.Groups["name"].Value;
            size = 0;
            long.TryParse(m.Groups["size"].Value, out size);
            permissions = m.Groups["permission"].Value;
        }

        public bool IsDirectory
        {
            get { return isDir; }
        }

        public string Name
        {
            get { return name; }
        }

        public long Size
        {
            get { return size; }
        }

        public string Permissions
        {
            get { return permissions; }
        }

        public string Directory
        {
            get { return directory; }
        }

        public string Path
        {
            get
            {
                if (isDir)
                    return directory + HttpUtility.UrlPathEncode(name) + "/";
                else
                    return directory + HttpUtility.UrlPathEncode(name);
            }
        }

        #region Regular Expressions
        static readonly string[] regexFormats = new string[] {
                @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\w+\s+\w+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)", 
                @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\d+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)", 
                @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\d+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)", 
                @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+\w+\s+\w+\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)", 
                @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})(\s+)(?<size>(\d+))(\s+)(?<ctbit>(\w+\s\w+))(\s+)(?<size2>(\d+))\s+(?<timestamp>\w+\s+\d+\s+\d{2}:\d{2})\s+(?<name>.+)", 
                @"(?<timestamp>\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+(?<dir>\<\w+\>){0,1}(?<size>\d+){0,1}\s+(?<name>.+)"
             };

        static Match GetMatchingRegex(string line)
        {
            foreach (string format in regexFormats)
            {
                Match match = new Regex(format).Match(line);
                if (match.Success)
                    return match;
            }
            throw new ApplicationException("Unexpected result from server: " + line);
        }
#endregion
    }
}
